我們現在可以選擇要使用什麼資料庫了,今天就來把 Attribube
加上去吧!疑,等等,我們不是在前幾天已經做過了 Attribute
嗎? (你是不是在偷篇數),對,沒錯,我們的確是已經完成了 Attribube
,但那是用 method_missing
搭配 define_method
做出來的,不過其實還有效能更快的做法,就是使用 class_eval
有些讀者可能會覺得,那這樣為什麼一開始不直接寫使用 class_eval
呢?其實這個系列也是我邊看書和影片,邊找資料學習的過程,希望透過各種方式,嘗試許多做法,來了解其中的差異
現在我們就來直接進入正題吧!
但其實也不是 class_eval
就特別的快,應該說各有各的優缺點,依照你的架構需求來選擇要用那種方式來做到 Dynamic Method Definitions
的效果,而在 Rails 比較常見到的是 class_eval
,我不太確定 Rails 真正選擇 class_eval
的用意,但我們可以透過比較來知道兩個的差異
我們就用 benchmark
來測試看看效能差異吧!
# demo/test.rb
require 'benchmark'
N = 100000
Benchmark.bm(7) do |x|
x.report("define_method") {
class Foo
N.times { |i| define_method("foo_#{i}") { } }
end
}
x.report("class_eval") {
class Foo
N.times { |i| class_eval "def bar_#{i}; end" }
end
}
end
我們先隨便開啟一個資料夾,並且寫兩個 benchmark
的測試,一個是用 define_method
來 定義方法
,另一個是用 class_eval
,接著來看看結果
user system total real
define_method 0.215727 0.015528 0.231255 ( 0.232347)
class_eval 1.401018 0.038479 1.439497 ( 1.443611)
會發現 class_eval
明顯慢很多,疑,休淡幾勒!你一開頭說 class_eval
效能比較好不是嗎?你幹嘛自己打自己臉...,先別急,這個速度指的是 Definition Performance
,也就是光指 定義方法
的效能表現,至於為什麼會有這樣的差異,文末的參考連結有比較詳細的探討這裡就不多加深入,只要知道一個結論就是,每執行一次 class_eval
會重新 compiles
一次 定義方法
的指令,但 define_method
不管定義幾個方法,都只會 compiles
一次
那既然 define_method
比較快,何不就用 define_method
就好,剛剛提到的是 定義方法
,但呼叫方法可不一樣了,讓我們接著看看下一個測試
# demo/test.rb
require 'benchmark'
class Foo
define_method("foo") { }
class_eval 'def bar; end'
end
N = 100000
Benchmark.bm(7) do |x|
foo = Foo.new
x.report("define_method") {
N.times { foo.bar }
}
x.report("class_eval") {
N.times { foo.bar }
}
end
我們用 define_method
和 class_eval
各定義了一個方法來執行,看看結果
user system total real
define_method 0.004096 0.000003 0.004099 ( 0.004095)
class_eval 0.004042 0.000002 0.004044 ( 0.004043)
好像差不多耶?但我們在定義的 方法
裡面並沒有做任何事情,現實生活中多少會在 方法
裡面做一些運算,讓我們加點工作在 方法
裡面再試試看吧
# demo/test.rb
require 'benchmark'
class Foo
define_method("foo") { 10.times.map { "foo".length } }
class_eval 'def bar; 10.times.map { "foo".length }; end'
end
N = 100000
Benchmark.bm(7) do |x|
foo = Foo.new
x.report("define_method")
N.times { foo.bar }
}
x.report("class_eval") {
N.times { foo.bar }
}
end
我們在各自的方法裡面加了一點簡單的運算工作,來看看結果
user system total real
define_method 0.144662 0.000705 0.145367 ( 0.146443)
class_eval 0.138658 0.000306 0.138964 ( 0.139517)
嗯!很明顯的出現差距了,用 class_eval
所定義出來的 method 執行速度會比較快
我們知道用 class_eval
來 定義方法
雖然定義的速度會比較慢,但定義完後 執行
方法裡面的運算卻比較快
現在我們就來仿照 Rails,來實作動態定義 Attribute
,首先打開 persistence.rb
,我們這次加強版的內容除了支援 postgresql
,也支援新的定義 Attribute
的方式
# mavericks/lib/ mavericks/data_record/persistence.rb
module Mavericks
module DataRecord
module Persistence
def initialize(attributes = {})
self.class.set_column_to_attribute
@attributes = attributes
end
end
end
end
我們讓繼承的 class,例如 Task
,可以在 new 的時候就定義好 Attribute
,定義的方式寫在set_column_to_attribute
def set_column_to_attribute
columns = self.connection.execute("SELECT column_name FROM information_schema.columns
WHERE table_name= '#{self.table_name}'").map{|m| m["column_name"]}
columns.each{ |column| define_method_attribute(column) }
end
最後用 class_eval 來執行 attribue
def define_method_attribute(name)
class_eval <<-STR
def #{name}
@attributes[:#{name}] || @attributes["#{name}"]
end
def #{name}=(value)
@attributes[:#{name}] = value
end
STR
end
實作原理跟前面不管是 sqlite_model.rb
或是 file_model.rb
都很類似就不多說明,差別在於我們這次用 class_eval
,而且是在 new 一個物件時就產生,所以不會有 method_missing
的效能問題產生
接著一樣在 just_do 的 sqlite_test.rb 測試看看
# just_do/sqlite_test.rb
require 'mavericks/data_record'
Mavericks::DataRecord::Base.establish_connection
class Task < Mavericks::DataRecord::Base
end
task = Task.new(title: '鐵人30')
puts task.title
# 鐵人30
task.title = '鐵人40'
puts task.title
# 鐵人40
如果成功印出內容,代表我們成功了!